package com.spzx.product.service.impl;

import com.spzx.common.core.utils.StringUtils;
import com.spzx.product.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class TestServiceImpl implements TestService {

   @Autowired
   private StringRedisTemplate stringRedisTemplate;

   //分布式锁，使用redis来实现分布式锁
   /**
    * 这种利用redis来实现分布式锁的方式有一个致命问题，
    * 现在的环境情况是有三台服务器A,B,C集群同时来获取锁
    * 此时如果A服务器获取到了锁，然后A服务器遇到了业务异常，导致无法释放锁
    * 那么就将会导致B服务器和C服务器无法获取到锁，从而导致业务无法正常执行
    * 我们可以使用finally 来释放锁
    * 但是如果A服务器宕机了，此时无法执行finally语句来释放锁，那么就会导致业务无法正常执行
    * ——> 此时我们可以给redis中的lock设置一个过期时间，当锁过期后，自动释放锁
    */
   @Override
   public void testLock1() {
      //尝试获取锁
//      Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock");
      //给锁设置过期时间
//      stringRedisTemplate.expire("lock",5,TimeUnit.SECONDS);
      //【一步到位】 ——>但是要注意，这里如果我们给lock设置过期时间
      // 那么我们必须把finally语句注释掉，因为如果lock已经过期删除了，那么再在finally语句中删除lock，那么就会报错
      /**
       * 这样子给锁设置过期时间的方式，就这样几点缺点
       * 1.如果我们过期时间设置的太短，导致A获取到锁执行方法还没执行完，锁就被释放了
       * 那么其他服务器就会趁虚而入获取到锁，这样等于没加锁
       * 2.如果我们设置过期时间太长，那么就会导致系统的效率很差，因为其他服务器要等待锁过期了才能获取到锁
       */
      Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock",5,TimeUnit.SECONDS);
      //若flag为true，说明成功获取到锁了
      if (flag){
          try {
              //成功获取到锁，优先判断num值是否合法
              String value = stringRedisTemplate.opsForValue().get("num");
              if (StringUtils.isBlank(value)){
                 return;
              }
              //对num值进行加一操作
              int num = Integer.parseInt(value);
              stringRedisTemplate.opsForValue().set("num",String.valueOf(++num));
              stringRedisTemplate.delete("lock");
          } catch (NumberFormatException e) {
             System.out.println("捕获到了异常");
              e.printStackTrace();
          }
          /*
          finally {
             //完成操作了，现在需要释放锁
             //System.out.println("释放锁咯");
             //stringRedisTemplate.delete("lock");
          }
          */

      } else {
          try {
             //若获取不到锁，则等待1秒再尝试获取锁
              Thread.sleep(1000);
              testLock1();
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
   }


   /**
    * 上面的testLock1中，我们使用redis来实现分布式锁，并且对分布式锁进行了改进
    * 1.我们通过设置finally语句，来防止某台服务器因为业务异常，导致无法释放锁--->已废弃
    *    缺点：如果遇到极端情况，获取到锁的服务器宕机，那么其他服务器无法获取到锁，导致业务无法正常执行
    * 2.我们通过给redis中的锁设置过期时间，当锁过期后，自动释放锁--->已废弃
    *    缺点：1.难以把握锁的过期时间的长短设置，若过期时间设置的太长了，就会导致系统效率差
    *         2.如果锁的过期时间设置的过短，就会导致获取到锁的服务器执行方法还没执行完，锁就被释放了
    * 因此为了更进一步，优化分布式锁，我们可以尝试下面这个思路：
    *  - 我们依旧使用给锁设置过期时间的方式，但是我们无需担心如何把控时间的长短，我们推荐给过期时间设置的长一点，防止代码还没执行完，锁就过期了
    *  - 我们给redis中的锁设置的值为一个UUID，每台服务器获取到的锁都是独一无二的
    *  - 当获取到锁的服务器执行完业务代码，准备释放锁的时候，我们可以再次查询lock的值，来判断这把锁是否还在自己手里
    *  - 因为如果此时锁提前过期了，被其他的服务器获取到，那么lock的值会被覆盖掉，不是先前的uuid
    *  - 如果此时lock的值依旧还是我们自己设置的uuid，那么就说明这把锁还是我们的锁，那么就可以放心的释放锁了
    */
   @Override
   public void testLock2() {
      //给锁设置一个独一无二的名字
      String keyName = UUID.randomUUID().toString();
      //尝试获取锁
      Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", keyName, 5, TimeUnit.SECONDS);
      //获取到锁咯~~~
      if (flag){
         log.info("获取到锁了");
         //成功获取到锁，优先判断num值是否合法
         String value = stringRedisTemplate.opsForValue().get("num");
         if (StringUtils.isBlank(value)){
            log.error("num的值为空");
            return;
         }
         //对num值进行加一操作
         int num = Integer.parseInt(value);
         stringRedisTemplate.opsForValue().set("num",String.valueOf(++num));
         //现在代码执行完了，我们准备释放锁，但是在释放的时候要判断当前锁是否是自己的锁
         if (keyName.equals(stringRedisTemplate.opsForValue().get("lock"))){
            log.info("释放锁......");
            //如果此时获取到的锁的值依旧是我们设置的uuid，那么就说明锁还没有过期被其他服务器获取到，我们可以放心释放锁
            stringRedisTemplate.delete("lock");
         }
      } else {
         //代码走到这里说明锁被其他服务器占用了，那么我们就在这里重复获取锁
          try {
              Thread.sleep(100);
             log.info("重试获取锁中......");
              testLock2();
          } catch (InterruptedException e) {
              throw new RuntimeException(e);
          }
      }
      /**
       * 结尾小结:
       * 这种方式虽然解决了过期时间设置的问题，但是它也存在一个致命的缺点：
       * 那就是我们对lock进行删除的时候:stringRedisTemplate.delete("lock")
       * 删除操作，缺乏 原！子！性！
       * 有一种极端情况：
       * 1.A服务器获取到锁，执行完了业务代码，准备释放锁了，在删除lock的时候，突然！lock到达了过期时间，自动删除了
       * 2.然后此时B服务器趁虚而入，获取到了锁，并且执行代码...
       * 3.然后此时，A服务器剩下的删除操作继续执行了，stringRedisTemplate.delete("lock");
       * 4.然后，这个时候就会把B服务器的锁给删除了，然后导致B服务器的锁白加了，其他服务器继续争抢锁......依旧线程不安全
       * 5.因此！我们要把过期时间设置的长一些，防止上述情况的发生
       * 6.或者......使用lua脚本，来保证原子性，具体方案入testLock3()所示
       */
   }


    /**
     * 使用LUA脚本进行优化
     * 保证释放锁的原子性
     */
    @Override
    public void testLock3() {
        //给锁设置一个独一无二的名字
        String keyName = UUID.randomUUID().toString();
        //尝试获取锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", keyName, 5, TimeUnit.SECONDS);
        //获取到锁咯~~~
        if (flag){
            log.info("获取到锁了");
            //成功获取到锁，优先判断num值是否合法
            String value = stringRedisTemplate.opsForValue().get("num");
            if (StringUtils.isBlank(value)){
                log.error("num的值为空");
                return;
            }
            //对num值进行加一操作
            int num = Integer.parseInt(value);
            stringRedisTemplate.opsForValue().set("num",String.valueOf(++num));
            //现在代码执行完了，我们准备释放锁，但是在释放的时候要判断当前锁是否是自己的锁
            //使用LUA脚本来同时进行判断是否同一把锁，并且释放锁
            String script = """
                    if redis.call('get',KEYS[1]) == ARGV[1] then
                        return redis.call('delete',KEYS[1])
                    else
                        return 0
                    end
                    """;
            Boolean lock = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
                    Arrays.asList("lock"),
                    keyName);
            if (lock){
                log.info("释放锁成功");
            } else {
                log.info("释放锁失败");
            }
        } else {
            //代码走到这里说明锁被其他服务器占用了，那么我们就在这里重复获取锁
            try {
                Thread.sleep(100);
                log.info("重试获取锁中......");
                testLock2();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }



   /*
    //通过同步锁(synchronized)来解决线程冲突问题
    //结论：在单服务器(一个JVM实例)情况下，synchronized是线程安全的，
    //但是多服务器(多个JVM实例，集群环境)的情况下，synchronized是不安全的
    //在集群环境中，每个服务器有自己的内存空间，synchronized 无法跨内存空间工作
   @Override
   public synchronized void testLock() {
      // 查询Redis中的num值
      String value = this.stringRedisTemplate.opsForValue().get("num");
      // 没有该值return
      if (StringUtils.isBlank(value)){
         return ;
      }
      // 有值就转成int
      int num = Integer.parseInt(value);
      // 把Redis中的num值+1
      this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
      //stringRedisTemplate.opsForValue().increment("num");
   }
   */




}